﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Xunit;
using YAPL.Configuration;
using YAPL.Language.Ast.Transformation;
using YAPL.Language.Compiler;
using YAPL.Language.Compiler.CodeGen.ReflectionEmitGenerator;
using YAPL.Language.Ast;
using YAPL.Language.Compiler.ILOptimization;
using YAPL.Util;

namespace YAPL.Test.YAPL.Language.Compiler.CodeGen.ReflectionEmitGenerator {
	public class GenerateCodeTest {
		private const string OUTPUT_PATH = "test_result.exe";
		private readonly ReCodeGenerator _generator;
		private readonly ILVerifier _ilVerifier;

		public GenerateCodeTest() {
			var outputInfo = new OutputInfo(OUTPUT_PATH, OutputType.NetConsoleApplication);
			_generator = new ReCodeGenerator(outputInfo);
			_ilVerifier = TestHelper.GetVerificationHelper();
		}
		
		[Fact]
		public void EmptyProgramShouldBeValid() {
			var expr = TestHelper.GetAstFromYAPL(FixturePath.EmptyProgram_yapl);
			GenerateCodeAndVerify(expr);
		}

		[Fact]
		public void FunctionCallNoParamsProgramShouldBeValid() {
			var expr = TestHelper.GetAstFromYAPL(FixturePath.FunctionCallNoParams_yapl);
			GenerateCodeAndVerify(expr);
		}


		[Fact]
		public void HelloWorldProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL(FixturePath.HelloWorld_yapl);
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.True(output.Length > 0);
			Assert.True(output.Contains("Hello, World!"));
		}

		[Fact]
		public void AssignmentAndVariablesProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL(FixturePath.AssignmentAndVariables_yapl);
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.True(output.Length > 0);
			Assert.True(output.Contains("a variable"));
			Assert.True(output.Contains("b variable"));
		}

		[Fact]
		public void SimpleTypesProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL(FixturePath.SimpleTypes_yapl);
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.True(output.Length > 0);
			Assert.True(output.Contains("234"));
		}

		[Fact]
		public void UnaryOperatorsProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL(FixturePath.UnaryOperators_yapl);
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.True(output.Length > 0);
			Assert.True(output.Contains("-11"));
		}

		[Fact]
		public void SimpleMathProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL(FixturePath.SimpleMath_yapl);
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.True(output.Contains("-30"));
		}

		[Fact]
		public void NestedFunctionCallProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL(FixturePath.NestedFunctionCall_yapl);
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.True(output.Contains("constant string"));
		}

		[Fact]
		public void PrintProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL(FixturePath.Print_yapl);
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.True(output.Contains("2.2 multiplied by 4 = 8.8"));
		}

		[Fact]
		public void FunctionWithParamsProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL(FixturePath.FunctionWithParams_yapl);
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.True(output.Contains("6"));
		}

		[Fact]
		public void ArithmeticProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.Arithmetic_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			var number = double.Parse(output.Split('=')[1],new NumberFormatInfo());
			Assert.InRange(number, -0.11 ,- 0.09);
		}

		[Fact]
		public void TypeConversionProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.TypeConversion_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			var oStrings = output.Split(new[] {":","\r\n"}, StringSplitOptions.RemoveEmptyEntries);
			Assert.Equal("YAPLString", oStrings[0]);
			Assert.Equal("YAPLFloat", oStrings[2]);
			Assert.Equal("YAPLInt", oStrings[4]);
			Assert.Equal("123.4", oStrings[1]);
			Assert.Equal(123.4, double.Parse(oStrings[3], new NumberFormatInfo()));
			Assert.Equal(123, int.Parse(oStrings[5]));
		}

		[Fact]
		public void CallingMethodOnSimpleTypeProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.CallingMethodOnSimpleType_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			var oStrings = output.Split(new[] { ":", "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
			Assert.Equal("esrever ot gnirts", oStrings[1]); //string to reverse
		}

		[Fact]
		public void SimpleClassProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.SimpleClass_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.Equal("class test\r\n", output);
		}

		[Fact]
		public void FunctionInsideCodeBlockProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.FunctionInsideCodeBlock_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.True(output.Contains("10"));
		}

		[Fact]
		public void VectorClassProgramShouldBeValid() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.VectorClass_yapl));
			GenerateCodeAndVerify(expr);
		}

		[Fact]
		public void ClassInitializerProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromXML((FixturePath.ClassInitializer_xml));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.Equal("Hello, World!\r\n", output);
		}

		[Fact]
		public void NoMethodProgramShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.NoMethod_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.Equal("method not found: f()\r\n", output);
		}

		[Fact]
		public void StaticMethodsShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.StaticMethods_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.Equal("inside static func1\r\ninside static func2\r\n", output);
		}

		[Fact]
		public void DynamicAddMethodShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.DynamicAddMethod_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.Equal("dynamic add\r\nmethod not found: dynFunc()\r\n", output);
		}

		[Fact]
		public void InstanceVariableFuncCallShouldBeValidAndPrintCorrectResult()
		{
			var expr = TestHelper.GetAstFromYAPL((FixturePath.InstanceVariableFuncCall_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.Equal("f1\r\n", output);
		}

		[Fact]
		public void InstanceVariableFuncDefShouldBeValidAndPrintCorrectResult()
		{
			var expr = TestHelper.GetAstFromYAPL((FixturePath.InstanceVariableFuncDef_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.Equal("f2\r\n", output);
		}

		[Fact]
		public void BooleanUsageShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.BooleanUsage_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.Equal("false\r\n", output);
		}

		[Fact]
		public void SimpleIfShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.SimpleIf_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.Equal("then branch\r\n", output);
		}

		[Fact]
		public void SimpleIfElseShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.SimpleIfElse_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.Equal("else branch\r\n", output);
		}

		[Fact]
		public void WhileShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.While_yapl));
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.Equal("3\r\n2\r\n1\r\n", output);
		}

		[Fact]
		public void ConstantFormulaShouldBeValidAndPrintCorrectResult() {
			var expr = TestHelper.GetAstFromYAPL((FixturePath.ConstantFormula_yapl));
			if (Config.Instance.GetBool(Config.AST_OPTIMIZATIONS_ENABLED)) {
				new AstOptimizer().Optimize(expr);
			}
			
			GenerateCodeAndVerify(expr);

			var output = Executor.GetProgramOutput(OUTPUT_PATH);
			Assert.Equal("21,2 dollars in my p0cket\r\n", output);
		}

		#region helper methods
		private void GenerateCodeAndVerify(Expression expr) {
			_generator.GenerateCode(expr);
			var result = _ilVerifier.VerifyAssemply(OUTPUT_PATH);
			Assert.Equal(ILVerifier.GetOkVerificationMessage(OUTPUT_PATH), result);

			//verify after optimizations
			if (Config.Instance.GetBool(Config.CIL_OPTIMIZATIONS_ENABLED)) {
				var optimizer = new PeepholeOptimizer(OUTPUT_PATH);
				optimizer.RewriteAssembly();
				result = _ilVerifier.VerifyAssemply(OUTPUT_PATH);
				Assert.Equal(ILVerifier.GetOkVerificationMessage(OUTPUT_PATH), result);
			}
		}
		#endregion
	}
}